<template>
  <div
    ref="draggableContainer"
    class="draggable-container"
    :style="containerStyle"
  >
    <template v-for="item of items">
      <slot :item="item"></slot>
    </template>
  </div>
</template>

<script>
import { cloneDeep } from "lodash";
import Sortable from "sortablejs";

/**
 * 基于 sortable.js 可拖拽容器
 */
export default {
  name: "DraggableContainer",

  model: {
    prop: "items",
    event: "update:items",
  },

  props: {
    items: {
      type: Array,
      required: true,
    },

    /**
     * 支持 sortable.js 属性
     */
    options: {
      type: Object,
      default: () => ({}),
    },

    /**
     * 自定义样式
     */
    containerStyle: {
      type: Object,
      default: () => ({}),
    },
  },

  watch: {
    "options.disabled"(disabled) {
      this.sortable.option("disabled", disabled);
    },
  },

  mounted() {
    this.initSortable();
  },

  beforeDestroy() {
    if (this.sortable) {
      this.sortable.destroy();
    }
  },
  methods: {
    initSortable() {
      const group =
        typeof this.options.group === "string"
          ? { pull: "clone", name: this.options.group }
          : { pull: "clone", ...this.options.group };
      this.sortable = Sortable.create(this.$refs.draggableContainer, {
        animation: 100,
        scroll: true,
        forceFallback: true,
        forceAutoScrollFallback: true,
        scrollSensitivity: 60,
        scrollSpeed: 20,
        ghostClass: "drag-ghost-item", // 拖拽占位符样式
        chosenClass: "drag-chosen-item", // 选中元素时的样式
        fallbackClass: "drag-item", // 拖拽元素样式
        onStart: this.handleStartDrag.bind(this),
        onEnd: this.handleEndDrag.bind(this),
        onAdd: this.handleAdd.bind(this),
        onRemove: this.handleRemove.bind(this),
        onClone: this.handleClone.bind(this),
        ...this.options,
        group,
      });
    },

    /**
     * 给克隆元素添加样式
     * 也就是原地保留的元素样式
     */
    handleClone(event) {
      const cloneElement = event.clone;
      cloneElement.classList.add("drag-clone-item");
    },

    handleStartDrag(event) {
      const element = this.getDragElement(event);
      event.item._underlying_vm_ = cloneDeep(element);
      this.$emit("start", event);
    },

    getDragElement(event) {
      const dragDom = event.item;
      const children = this.$refs.draggableContainer.children;
      const index = Array.from(children).indexOf(dragDom);
      return this.items[index];
    },

    handleEndDrag(event) {
      this.$emit("end", event);
    },

    /**
     * 处理添加节点，适配 vue 数据管理，不让 sortable.js 直接操作 dom
     * 1. 获取拖拽实际元素
     * 2. 移除 sortable.js 添加的 dom 节点
     * 3. 更新 vue 数据
     */
    handleAdd(event) {
      const element = event.item._underlying_vm_;
      if (element === undefined) {
        return;
      }

      this.removeNode(event.item);
      this.items.splice(event.newIndex, 0, element);
    },

    /**
     * 处理删除节点，适配 vue 数据管理，不让 sortable.js 直接操作 dom
     * 1. 先恢复 dom
     * 2. 移除 clone 节点
     * 3. 更新 vue 数据
     */
    handleRemove(event) {
      this.insertNodeAt(
        this.$refs.draggableContainer,
        event.item,
        event.oldIndex
      );
      this.removeNode(event.clone);
      this.items.splice(event.oldIndex, 1);
      this.$emit("update:items", this.items);
    },

    /**
     * 从 DOM 中移除被拖拽的元素节点
     */
    removeNode(node) {
      if (node.parentElement !== null) {
        node.parentElement.removeChild(node);
      }
    },

    /**
     * 在指定位置插入 DOM 节点
     */
    insertNodeAt(fatherNode, node, position) {
      const refNode =
        position === 0
          ? fatherNode.children[0]
          : fatherNode.children[position - 1].nextSibling;
      fatherNode.insertBefore(node, refNode);
    },
  },
};
</script>

<style scoped>
.draggable-container {
  display: flex;
  flex-direction: column;
}

/** 拖拽保留的元素样式 */
.drag-clone-item {
  background-color: transparent !important;
  border: 2px dashed #409eff !important;
  color: transparent !important;
}

/** 拖拽占位符样式 */
.drag-ghost-item {
  border: 2px dashed #409eff !important;
  background-color: pink !important;
  color: transparent !important;
}

.drag-ghost-item * {
  border-color: transparent !important;
  background-color: transparent !important;
  color: transparent !important;
}

/** 拖拽元素样式 */
.drag-item {
  opacity: 1 !important;
}
</style>
